1 module node_dlang; 2 3 public import js_native_api_types : napi_env, napi_value, napi_callback; 4 import node_api; 5 import js_native_api; 6 7 import std.conv : to, text; 8 import std.string : toStringz; 9 import std.traits; 10 import std.algorithm; 11 debug import std.stdio; 12 13 import std.variant; 14 template isVariantN (alias T) { 15 enum isVariantN = __traits(isSame, TemplateOf!T, VariantN); 16 } 17 18 auto napiIdentity (napi_env _1, napi_value value, napi_value * toRet) { 19 *toRet = value; 20 return napi_status.napi_ok; 21 } 22 23 alias ExternD (T) = SetFunctionAttributes!(T, "D", functionAttributes!T); 24 alias ExternC (T) = SetFunctionAttributes!(T, "C", functionAttributes!T); 25 26 auto toNapiValueArray (T ...)(napi_env env, T args) { 27 napi_value [args.length] argVals; 28 foreach (i, arg; args) { 29 // Create a temporary value with the casted data. 30 argVals [i] = arg.toNapiValue (env); 31 } 32 return argVals; 33 } 34 35 auto constructor (RetType, T ...)(napi_env env, napi_value constructorNapi, T args) { 36 const asArr = toNapiValueArray (env, args); 37 napi_value toRet; 38 auto status = napi_new_instance ( 39 env 40 , constructorNapi 41 , asArr.length 42 , asArr.ptr 43 , &toRet 44 ); 45 assert (status == napi_status.napi_ok, `Error calling JS constructror`); 46 return fromNapi!RetType (env, toRet); 47 } 48 49 // If the first argument to the delegate (in other words, R) is a napi_value 50 // that is used as the context. 51 // Otherwise the global context is used. 52 auto jsFunction (R)(napi_env env, napi_value func, R * toRet) { 53 alias Params = Parameters!R; 54 alias RetType = ReturnType!R; 55 // Cannot assign toRet here for extern(C) Rs or LDC complains :( 56 auto foo = delegate RetType (Params args) { 57 static if (args.length > 0 && is (Params [0] == napi_value)) { 58 // Use provided context. 59 auto status = napi_status.napi_generic_failure; 60 napi_value context = args [0]; 61 enum firstArgPos = 1; 62 } else { 63 napi_value context; 64 auto status = napi_get_global (env, &context); 65 assert (status == napi_status.napi_ok); 66 enum firstArgPos = 0; 67 } 68 /+napi_value [args.length - firstArgPos] napiArgs; 69 70 foreach (i, arg; args [firstArgPos..$]) { 71 napiArgs [i] = arg.toNapiValue (env); 72 }+/ 73 auto napiArgs = toNapiValueArray (env, args [firstArgPos .. $]); 74 napi_value returned; 75 status = napi_call_function ( 76 env 77 , context 78 , func 79 , napiArgs.length 80 , napiArgs.ptr 81 , &returned 82 ); 83 if (status != napi_status.napi_ok) { 84 debug stderr.writeln (`Got status `, status); 85 throw new Exception (`JS call errored :(`); 86 } 87 88 static if (!is (RetType == void)) { 89 return fromNapi!RetType (env, returned); 90 } 91 }; 92 *toRet = cast (R) foo; 93 return napi_status.napi_ok; 94 } 95 96 auto getDgPointer (FP)(napi_env env, napi_value func, FP * toRet) { 97 static if (is (FP == F*, F)) { 98 import std.conv : emplace; 99 char [F.sizeof] buf; 100 /+ 101 auto c = emplace!C(buf, 5); 102 auto intermediatePtr = new F [1].ptr; 103 jsFunction (env, func, intermediatePtr); 104 *toRet = intermediatePtr; 105 +/ 106 } else static assert (0); 107 return napi_status.napi_ok; 108 } 109 110 auto reference (napi_env env, ref napi_value obj) { 111 napi_ref toRet; 112 auto status = napi_create_reference (env, obj, 1, &toRet); 113 if (status != napi_status.napi_ok) throw new Exception ( 114 text (`Reference creation failed: `, status) 115 ); 116 return toRet; 117 } 118 119 // Note this might escape values out of scope 120 auto val (napi_env env, napi_ref reference) { 121 napi_value toRet; 122 auto status = napi_get_reference_value (env, reference, &toRet); 123 if (status != napi_status.napi_ok) throw new Exception (`Could not get value from reference`); 124 assert (toRet != null); 125 return toRet; 126 } 127 128 class JSException : Exception { 129 napi_value jsException; 130 this ( 131 napi_value jsException 132 , string message = `JS Exception` 133 , string file = __FILE__ 134 , size_t line = __LINE__ 135 , Throwable nextInChain = null) { 136 this.jsException = jsException; 137 super (message, file, line, nextInChain); 138 } 139 } 140 141 auto callNapi (Args ...)(napi_env env, napi_value context, napi_value func, Args args) { 142 napi_value [args.length] napiArgs; 143 static foreach (i, arg; args) { 144 napiArgs [i] = arg.toNapiValue (env); 145 } 146 napi_value returned; 147 auto status = napi_call_function (env, context, func, args.length, napiArgs.ptr, &returned); 148 if (status == napi_status.napi_pending_exception) { 149 napi_value exceptionData; 150 debug stderr.writeln (`Got JS exception`); 151 napi_get_and_clear_last_exception (env, &exceptionData); 152 throw new JSException (exceptionData); 153 } else if (status != napi_status.napi_ok) { 154 debug stderr.writeln (`Got JS status `, status); 155 throw new Exception (text (`Call errored for args `, args)); 156 } 157 return returned; 158 } 159 160 // Get a property. 161 auto p (RetType = napi_value, S) (napi_value obj, napi_env env, S propName) 162 if (isSomeString!S) { 163 napi_value toRet; 164 auto key = propName.toNapiValue (env); 165 auto status = napi_get_property (env, obj, key, &toRet); 166 if (status != napi_status.napi_ok) { 167 throw new Exception (`Failed to get property ` ~ propName); 168 } 169 return fromNapi!RetType (env, toRet); 170 } 171 // Assign a property. 172 void p (InType, S) (napi_value obj, napi_env env, S propName, InType newVal) 173 if (isSomeString!S) { 174 auto key = propName.toNapiValue (env); 175 auto status = napi_set_property (env, obj, key, newVal.toNapiValue (env)); 176 if (status != napi_status.napi_ok) { 177 throw new Exception (`Failed to set property ` ~ propName.to!string); 178 } 179 } 180 181 private { 182 struct NapiRefWithId { napi_ref v; } 183 struct NapiValWithId { napi_value v; } 184 } 185 186 struct JSVar { 187 napi_env env; 188 // DMD seems to handle well the difference between napi_ref and napi_value 189 // but LDC doesn't, so wrapped the types. 190 alias CtxRefT = Algebraic! (NapiRefWithId, NapiValWithId); 191 CtxRefT ctxRef; 192 this (napi_env env, napi_value nVal) { 193 assert (env != null && nVal != null); 194 this.env = env; 195 napi_valuetype napiType; 196 assert (napi_typeof(env, nVal, &napiType) == napi_status.napi_ok); 197 if (napiType == napi_valuetype.napi_object) { 198 this.ctxRef = NapiRefWithId (reference (env, nVal)); 199 } else { 200 // TODO: Check cases such as functions which might be problematic. 201 this.ctxRef = NapiValWithId (nVal); 202 } 203 } 204 this (T) (napi_env env, T val) { 205 this (env, val.toNapiValue (env)); 206 } 207 208 auto constructor (RetType = JSVar, T ...)(T args) { 209 return .constructor!RetType (this.env, this.context (), args); 210 } 211 212 auto context () { 213 return ctxRef.visit! ( 214 (NapiValWithId v) => v.v 215 , (NapiRefWithId r) => val (env, r.v) 216 ); 217 } 218 219 auto toNapiValue (napi_env env) { 220 assert (this.context () != null); 221 return this.context (); 222 } 223 224 auto opIndex (S)(S propName) if (isSomeString!S) { 225 return context.p!JSVar (env, propName); 226 } 227 228 auto opIndexAssign (T, S)(T toAssign, S propName) if (isSomeString!S) { 229 context ().p (env, propName, toAssign); 230 } 231 232 template opDispatch (string s) { 233 R opDispatch (R = JSVar, T...)(T args) { 234 auto ctx = this.context (); 235 auto toCallAsNapi = ctx.p (env, s); 236 auto asCallable = fromNapi! (R delegate (napi_value, T))(env, toCallAsNapi); 237 static if (is (R == void)) { 238 asCallable (ctx, args); 239 } else { 240 return asCallable (ctx, args); 241 } 242 } 243 } 244 245 // Convenience console.log function 246 auto jsLog () { 247 console (this.env).log (this.context ()); 248 } 249 250 bool isUndefined () { 251 return .isUndefined (this.env, this.context ()); 252 } 253 254 auto opCast (T) () { 255 return fromNapi!T (env, this.context ()); 256 } 257 258 auto opCall (R = JSVar, T ...) (T args) { 259 auto ctx = this.context (); 260 auto toCall = fromNapi! (R delegate (napi_value, T)) (env, ctx); 261 return toCall (ctx, args); 262 } 263 } 264 265 struct Promise { 266 @disable this (); 267 napi_deferred deferred; 268 JSVar promise; 269 napi_env env; 270 this (napi_env env) { 271 this.env = env; 272 napi_value napiPromise; 273 auto status = napi_create_promise (env, &deferred, &napiPromise); 274 promise = JSVar (env, napiPromise); 275 assert (status == napi_status.napi_ok); 276 } 277 void resolve (T)(T toRet) { 278 auto status = napi_resolve_deferred (env, deferred, toRet.toNapiValue (env)); 279 assert (status == napi_status.napi_ok); 280 } 281 void reject (T)(T toRet) { 282 auto status = napi_reject_deferred (env, deferred, toRet.toNapiValue (env)); 283 assert (status == napi_status.napi_ok); 284 } 285 napi_value toNapiValue (napi_env env) { 286 assert (env == this.env); 287 return this.promise.context (); 288 } 289 } 290 291 /// Similar to JSObj but doesn't have a reference counter, so cannot be used 292 /// after the JS call that has the scope where this was created. 293 /// Template is a struct type that contains fields and function declarations 294 /// that this struct will attempt to copy in signature but with JS type conversions. 295 /// Do note that accessing members is done lazily. 296 alias ScopedJSObj (Template) = JSObj!(Template, false); 297 298 struct Named { string name; }; 299 /// Stores a JS value with a reference counter so that JS's GC doesn't collect 300 /// it whilst it's stored in D. 301 /// Template is a struct type that contains fields and function declarations 302 /// that this struct will attempt to copy in signature but with JS type conversions. 303 /// Do note that accessing members is done lazily. 304 struct JSObj (Template, bool useRefCount = true) { 305 /// Convenience console.log (this) function 306 void jsLog () { 307 console (this.env).log (this.context); 308 } 309 310 void toString (scope void delegate (const (char)[]) sink) { 311 if (env is null || this.context is null) { 312 sink (`null JSObj`); 313 } else { 314 // Note: this could generate an error 315 napi_value asStr; 316 napi_coerce_to_string (env, this.context, &asStr); 317 sink (fromNapi!string (env, asStr)); 318 } 319 } 320 321 alias FieldNames = __traits (allMembers, Template); 322 private template nameForCaller (string name) { 323 alias nameUDAs = getUDAs! (mixin (`Template.` ~ name), Named); 324 static if (nameUDAs.length == 0) { 325 alias nameForCaller = name; 326 } else { 327 static assert (nameUDAs.length == 1, `Cannot have more than one Named UDA`); 328 enum nameForCaller = nameUDAs [0].name; 329 } 330 } 331 import std.meta; 332 // Equals to FieldNames unless the @named uda is used. 333 alias NamesForCaller = staticMap! (nameForCaller, FieldNames); 334 private template type (string name) { 335 alias type = typeof (mixin (`Template.` ~ name)); 336 } 337 alias FieldTypes = staticMap! (type, FieldNames); 338 private static auto positions () { 339 size_t [] funPositions; 340 size_t [] fieldPositions; 341 static foreach (i, Member; FieldNames) { 342 // Ignore opAssign, which might be implicitly generated 343 static if (Member != `opAssign` && !Member.startsWith (`__`)) { 344 static if (mixin (`isFunction! (Template.` ~ Member ~ `)`)) { 345 funPositions ~= i; 346 } else { 347 fieldPositions ~= i; 348 } 349 } 350 } 351 import std.typecons : tuple; 352 return tuple!(`funPositions`, `fieldPositions`) (funPositions, fieldPositions); 353 } 354 355 static assert (NamesForCaller.length == FieldTypes.length); 356 // Useful for type checking. 357 enum dlangNodeIsJSObj = true; 358 359 private enum Positions = positions (); 360 private enum FunPositions = Positions.funPositions; 361 private enum FieldPositions = Positions.fieldPositions; 362 363 napi_env env; 364 static if (useRefCount) { 365 napi_ref ctxRef = null; 366 auto context () { 367 return val (env, this.ctxRef); 368 } 369 } else { 370 napi_value context; 371 } 372 373 private enum typeMsg = `JSObj template args should be pairs of strings with types`; 374 // Creating a new object from D. 375 this (napi_env env) { 376 this.env = env; 377 static if (useRefCount) { 378 auto context = napi_value (); 379 auto status = napi_create_object (env, &context); 380 assert (status == napi_status.napi_ok); 381 ctxRef = reference (env, context); 382 } 383 } 384 385 // Assigning from a JS object. 386 this (napi_env env, napi_value context) { 387 this.env = env; 388 // Keep alive. Note this will NEVER be GC'ed 389 static if (useRefCount) { 390 ctxRef = reference (env, context); 391 } else { 392 this.context = context; 393 } 394 } 395 396 // Copy ctor. 397 this (ref return scope typeof (this) rhs) { 398 this.env = rhs.env; 399 static if (useRefCount) { 400 this.ctxRef = rhs.ctxRef; 401 if (ctxRef != null) { 402 auto status = napi_reference_ref (env, ctxRef, null); 403 //writeln (`> Ref count is `, currentRefCount); 404 assert (status == napi_status.napi_ok); 405 } 406 } else { 407 this.context = rhs.context; 408 } 409 } 410 ~this () { 411 // writeln ("Destructing JSobj " ~ exportsAlias.stringof); 412 // uint currentRefCount; 413 static if (useRefCount) { 414 if (ctxRef != null) { 415 auto status = napi_reference_unref (env, ctxRef, null); 416 assert ( 417 status == napi_status.napi_ok 418 , `Got status when doing unref ` ~ status.to!string 419 ); 420 //writeln (`< Ref count is `, currentRefCount); 421 } 422 } 423 } 424 425 static foreach (i, FunPosition; FunPositions) { 426 // Functions called constructor are equivalent to using new in JS. 427 static if (FieldNames [FunPosition] == `constructor`) { 428 pragma (msg, `Found constructor`); 429 static assert ( 430 is (ReturnType! (FieldTypes [FunPosition]) == void) 431 , `Please put void return type on JS constructor declarations, found ` 432 ~ ReturnType! (FieldTypes [FunPosition]).stringof 433 ); 434 auto constructor (Parameters! (FieldTypes [FunPosition]) args) { 435 return .constructor!(typeof (this)) (this.env, this.context (), args); 436 } 437 } else { 438 // Add function that simply uses callNapi. 439 mixin ( 440 q{auto } ~ NamesForCaller [FunPosition] ~ q{ (Parameters! (FieldTypes [FunPosition]) args) { 441 alias FunType = FieldTypes [FunPosition]; 442 alias RetType = ReturnType!(FunType); 443 //auto context = val (env, this.ctxRef); 444 auto toCall = context 445 .p! (RetType delegate (napi_value, Parameters!FunType)) 446 (env, NamesForCaller [FunPosition]); 447 static if (is (RetType == void)) { 448 toCall (context, args); 449 } else { 450 return toCall (context, args); 451 } 452 } 453 } 454 ); 455 } 456 } 457 static foreach (i, FieldPosition; FieldPositions) { 458 // Setter. 459 static if (isVariantN! (FieldTypes [FieldPosition])) { 460 // Also add implicit conversions :) 461 static foreach (PossibleType; TemplateArgsOf!(FieldTypes [FieldPosition])[1..$]) { 462 mixin (q{ 463 void } ~ NamesForCaller [FieldPosition] ~ q{ (PossibleType toSet) { 464 enum fieldName = NamesForCaller [FieldPosition]; 465 auto asNapi = toSet.toNapiValue (env); 466 auto propName = fieldName.toStringz; 467 //auto context = val (env, this.ctxRef); 468 auto status = napi_set_named_property (env, context, propName, asNapi); 469 assert (status == napi_status.napi_ok, `Couldn't set property ` ~ fieldName); 470 } 471 }); 472 }; 473 } 474 mixin (q{ 475 void } ~ FieldNames [FieldPosition] ~ q{ (FieldTypes [FieldPosition] toSet) { 476 enum fieldName = FieldNames [FieldPosition]; 477 auto asNapi = toSet.toNapiValue (env); 478 auto propName = fieldName.toStringz; 479 //auto context = val (env, this.ctxRef); 480 auto status = napi_set_named_property (env, context, propName, asNapi); 481 assert (status == napi_status.napi_ok, `Couldn't set property ` ~ fieldName); 482 } 483 }); 484 // Getter. 485 // Pretty similar in both cases, would like to deduplicate it but 486 // static foreach is fun. 487 static if (isVariantN! (FieldTypes [FieldPosition])) { 488 // Getting Algebraics/VariantNs must specify the returned type. 489 mixin (q{ 490 auto } ~ FieldNames [FieldPosition] ~ q{ (Type) () { 491 return fromNapi!Type ( 492 env 493 , context.p (env, FieldNames [FieldPosition]) 494 ); 495 } 496 }); 497 } else { 498 static if (isFunctionPointer! (FieldTypes [FieldPosition])) { 499 // Function pointers must become delegates because jsFunction 500 // adds a context. 501 // They also have a different signature so that two sets of parens 502 // aren't needed to call them. 503 mixin (q{auto } ~ FieldNames [FieldPosition] ~ q{ (Parameters!(FieldTypes [FieldPosition]) args) { 504 import std.functional : toDelegate; 505 alias RetType = typeof (FieldTypes [FieldPosition].init.toDelegate); 506 return fromNapi!RetType ( 507 env 508 , context.p (env, FieldNames [FieldPosition]) 509 ) (args); 510 }}); 511 } else { 512 // Other types use a direct getter function. 513 mixin (q{auto } ~ FieldNames [FieldPosition] ~ q{ () { 514 return fromNapi! (FieldTypes [FieldPosition]) ( 515 env 516 , context.p (env, FieldNames [FieldPosition]) 517 ); 518 }}); 519 } 520 } 521 } 522 } 523 524 // Convenience console type. 525 private struct Console_ { 526 void log (napi_value); 527 }; 528 alias Console = JSObj!Console_; 529 auto console = (napi_env env) => global!Console (env, `console`); 530 void jsLog (T)(napi_env env, T toLog) { 531 console (env).log (toLog.toNapiValue (env)); 532 } 533 auto global (napi_env env) { 534 napi_value val; 535 auto status = napi_get_global (env, &val); 536 assert (status == napi_status.napi_ok, `Couldn't get global context`); 537 return val; 538 } 539 540 auto global (RetType = JSVar)(napi_env env, string name) { 541 return fromNapi!RetType (env, global (env).p (env, name)); 542 } 543 544 auto getJSobj (T)(napi_env env, napi_value ctx, T * toRet) { 545 assert (toRet != null); 546 *toRet = T (env, ctx); 547 return napi_status.napi_ok; 548 } 549 550 auto getStr (StrType)(napi_env env, napi_value napiVal, StrType * toRet) { 551 // Try with statically allocated buffer for small strings 552 assert (toRet != null); 553 size_t readChars = 0; 554 static if (is (StrType == string)) { 555 alias CharType = char; 556 char [2048] inBuffer; 557 alias conversionFunction = napi_get_value_string_utf8; 558 } else static if (is (StrType == wstring)) { 559 alias CharType = wchar; 560 ushort [2048] inBuffer; 561 alias conversionFunction = napi_get_value_string_utf16; 562 } else static assert ( 563 false 564 , `N-API doesn't provide conversion for UTF-32 (dstrings), use string or wstring instead` 565 ); 566 auto status = conversionFunction ( 567 env 568 , napiVal 569 , inBuffer.ptr 570 , inBuffer.length 571 , & readChars 572 ); 573 if (status != napi_status.napi_ok) { 574 return status; 575 } 576 577 if (readChars == inBuffer.length - 1) { 578 // String bigger than buffer :( need a dynamic array. 579 // Technically this is not needed for the specific case of 580 // exactly size inBuffer.length - 1 581 // Get string size to allocate an array. 582 status = conversionFunction ( 583 env 584 , napiVal 585 , null 586 , 0 587 , & readChars 588 ); 589 assert (status == napi_status.napi_ok); 590 // Try again with bigger size. 591 auto buffer = new typeof(inBuffer [0]) [readChars + 1]; // include null terminator 592 status = conversionFunction ( 593 env 594 , napiVal 595 , buffer.ptr 596 , buffer.length 597 , & readChars 598 ); 599 assert (status == napi_status.napi_ok); 600 *toRet = buffer.ptr [0..readChars].map!(to!CharType).to!StrType; 601 } else { 602 // Got it on first try 603 *toRet = inBuffer.ptr [0..readChars].map!(to!CharType).to!StrType; 604 } 605 return napi_status.napi_ok; 606 } 607 608 auto getFloat (napi_env env, napi_value napiVal, float * toRet) { 609 double intermediate; 610 auto status = napi_get_value_double (env, napiVal, &intermediate); 611 *toRet = intermediate.to!float; 612 return status; 613 } 614 615 void inJSScope (alias fun)(napi_env env) { 616 napi_handle_scope jsScope; 617 auto status = napi_open_handle_scope (env, &jsScope); 618 assert (status == napi_status.napi_ok); 619 scope (exit) napi_close_handle_scope (env, jsScope); 620 fun (); 621 } 622 623 auto getAA (V)(napi_env env, napi_value napiVal, V [string] * toRet) { 624 * toRet = V [string].init; 625 napi_value propertyNames; 626 auto status = napi_get_property_names (env, napiVal, &propertyNames); 627 assert (status == napi_status.napi_ok); 628 uint propertyNamesLength; 629 status = napi_get_array_length (env, propertyNames, &propertyNamesLength); 630 assert (status == napi_status.napi_ok); 631 foreach (i; 0 .. propertyNamesLength) { 632 inJSScope! (() { 633 napi_value keyNapi; 634 status = napi_get_element (env, propertyNames, i, &keyNapi); 635 assert (status == napi_status.napi_ok); 636 napi_value element; 637 status = napi_get_property (env, napiVal, keyNapi, &element); 638 assert (status == napi_status.napi_ok); 639 (* toRet) [fromNapi!string (env, keyNapi)] = fromNapi!V (env, element); 640 }) (env); 641 } 642 return napi_status.napi_ok; 643 } 644 645 auto getJSVar (napi_env env, napi_value napiVal, JSVar * toRet) { 646 *toRet = JSVar (env, napiVal); 647 return napi_status.napi_ok; 648 } 649 650 auto getStaticArray (A)(napi_env env, napi_value napiVal, A * toRet) { 651 // Copying could be avoided 652 auto asDynamicArr = (* toRet) []; 653 auto toRetNapi = getArray (env, napiVal, & asDynamicArr); 654 foreach (i, val; asDynamicArr) { 655 (* toRet) [i] = val; 656 } 657 return toRetNapi; 658 } 659 660 auto getArray (A)(napi_env env, napi_value napiVal, A [] * toRet) { 661 import std.array; 662 Appender!(A []) toRetAppender; 663 uint arrLength; 664 auto status = napi_get_array_length (env, napiVal, &arrLength); 665 assert ( 666 status == napi_status.napi_ok 667 , `Error getting array length, maybe object isn't array or N-API errored on` 668 ~ ` the call before this one.` 669 ); 670 foreach (i; 0 .. arrLength) { 671 inJSScope! (() { 672 napi_value toConvert; 673 status = napi_get_element (env, napiVal, i, &toConvert); 674 assert (status == napi_status.napi_ok, `Couldn't get element from array`); 675 toRetAppender ~= fromNapi!A (env, toConvert); 676 }) (env); 677 } 678 *toRet = toRetAppender.data; 679 return napi_status.napi_ok; 680 } 681 682 auto getTypedArray (T)(napi_env env, napi_value napiVal, TypedArray!T * toRet) { 683 napi_typedarray_type type; 684 size_t length; 685 void * data; 686 napi_value arrayBuffer; 687 size_t offset; 688 auto status = napi_get_typedarray_info ( 689 env 690 , napiVal 691 , & type 692 , & length 693 , & data 694 , & arrayBuffer 695 , & offset 696 ); 697 698 enum expectedType = TypedArray!T.type; 699 assert ( 700 // ubyte accepts both clamped and unclamped arrays. 701 expectedType == type || (is (T == ubyte) 702 && type == napi_typedarray_type.napi_uint8_clamped_array) 703 , `TypedArray type doesn't match (make sure signedness is correct too)` 704 ); 705 toRet.internal = (cast (T *) data) [0 .. length]; 706 return status; 707 } 708 709 auto getStruct (S)(napi_env env, napi_value napiVal, S * toRet) { 710 *toRet = S.init; 711 foreach (fieldName; FieldNameTuple!S) { 712 alias FieldType = typeof (__traits (getMember, *toRet, fieldName)); 713 auto napiProp = napiVal.p (env, fieldName); 714 __traits (getMember, *toRet, fieldName) = fromNapi!FieldType (env, napiProp); 715 } 716 return napi_status.napi_ok; 717 } 718 719 template fromNapiB (T) { 720 static if (is (T == bool)) { 721 alias fromNapiB = napi_get_value_bool; 722 } else static if (is (T == double)) { 723 alias fromNapiB = napi_get_value_double; 724 } else static if (is (T == int)) { 725 alias fromNapiB = napi_get_value_int32; 726 } else static if (is (T == uint)) { 727 alias fromNapiB = napi_get_value_uint32; 728 } else static if (is (T == long)) { 729 alias fromNapiB = napi_get_value_int64; 730 } else static if (is (T == ulong)) { 731 alias fromNapiB = napi_get_value_bigint_uint64; 732 } else static if (is (T == double)) { 733 alias fromNapiB = napi_get_value_double; 734 } else static if (is (T == float)) { 735 alias fromNapiB = getFloat; 736 } else static if (is (T == TypedArray!A, A)) { 737 alias fromNapiB = getTypedArray; 738 } else static if (is (T == V [string], V)) { 739 alias fromNapiB = getAA; 740 } else static if (isSomeString!T) { 741 alias fromNapiB = getStr; 742 } else static if (is (T == Nullable!A, A)) { 743 alias fromNapiB = getNullable!A; 744 } else static if (is (T == napi_value)) { 745 alias fromNapiB = napiIdentity; 746 } else static if (isDelegate!T) { 747 alias fromNapiB = jsFunction; 748 } else static if (is (T == JSVar)) { 749 alias fromNapiB = getJSVar; 750 } else static if (isStaticArray!T) { 751 alias fromNapiB = getStaticArray; 752 } else static if (is (T == A[], A)) { 753 alias fromNapiB = getArray; 754 } else static if (is (T == A*, A) && isDelegate!A) { 755 alias fromNapiB = getDgPointer; 756 } else static if (isFunctionPointer!T) { 757 // Need context to perform JS calls. 758 static assert ( 759 0 760 , `Cannot receive function pointers, use delegates instead` 761 ); 762 } else static if (__traits(hasMember, T, `dlangNodeIsJSObj`)) { 763 alias fromNapiB = getJSobj; 764 } else static if (isVariantN!T) { 765 static assert ( 766 0 767 , `Don't use fromNapiB to get a VariantN/Algebraic, get the expected type instead` 768 ); 769 } else static if (__traits(isPOD, T)) { 770 alias fromNapiB = getStruct; 771 } else { 772 static assert (0, `Not implemented: Conversion from JS type for ` ~ T.stringof); 773 } 774 } 775 776 napi_status getNullable (BaseType) ( 777 napi_env env 778 , napi_value value 779 , Nullable!BaseType * toRet 780 ) { 781 auto erroredToJS = () => napi_throw_error ( 782 env, null, `Failed to parse to ` ~ Nullable!BaseType.stringof 783 ); 784 BaseType toRetNonNull; 785 auto status = fromNapiB!BaseType (env, value, &toRetNonNull); 786 if (status != napi_status.napi_ok) { 787 *toRet = Nullable!BaseType (); 788 } else { 789 *toRet = Nullable!BaseType (toRetNonNull); 790 } 791 return napi_status.napi_ok; 792 } 793 794 import std.typecons : Nullable; 795 /// Gets a D typed value from a napi_value 796 T fromNapi (T, string argName = ``)(napi_env env, napi_value value) { 797 T toRet; 798 auto erroredToJS = () => napi_throw_error ( 799 env, null, `Failed to parse ` ~ argName ~ ` to ` ~ T.stringof 800 ); 801 try { 802 auto status = fromNapiB!T (env, value, &toRet); 803 if (status != napi_status.napi_ok) { 804 erroredToJS (); 805 } 806 } catch (Exception ex) { 807 erroredToJS (); 808 } 809 return toRet; 810 } 811 812 void throwInJS (napi_env env, string message) { 813 napi_throw_error (env, null, message.toStringz); 814 } 815 816 napi_status boolToNapi (napi_env env, bool toConvert, napi_value * toRet) { 817 return napi_get_boolean (env, toConvert, toRet); 818 } 819 820 // O(n) operation. Use BufferArrays to avoid element-by-element JS object 821 // creation. 822 napi_status arrayToNapi (F)(napi_env env, F[] array, napi_value * toRet) { 823 assert (toRet != null); 824 auto status = napi_create_array_with_length (env, array.length, toRet); 825 assert (status == napi_status.napi_ok); 826 foreach (i, val; array) { 827 inJSScope! (() { 828 auto nv = val.toNapiValue (env); 829 status = napi_set_element (env, *toRet, i.to!uint, nv); 830 assert (status == napi_status.napi_ok); 831 }) (env); 832 } 833 return status; 834 } 835 836 /// Note: No conversion implemented for Uint8ClampedArray. 837 struct TypedArray (Element) { 838 /// Constructor that uses the provided internal array. 839 this (Element [] internal) { 840 this.internal = internal; 841 } 842 /// Constructor that allocates on JS mem. 843 this (napi_env env, uint length) { 844 auto buffer = global (env, `Uint8Array`).constructor (length); 845 this = cast (TypedArray!Element) buffer; 846 } 847 Element [] internal; 848 alias internal this; 849 static if (is (Element == byte)) { 850 enum type = napi_typedarray_type.napi_int8_array; 851 } else static if (is (Element == ubyte)) { 852 enum type = napi_typedarray_type.napi_uint8_array; 853 } else static if (is (Element == short)) { 854 enum type = napi_typedarray_type.napi_int16_array; 855 } else static if (is (Element == ushort)) { 856 enum type = napi_typedarray_type.napi_uint16_array; 857 } else static if (is (Element == int)) { 858 enum type = napi_typedarray_type.napi_int32_array; 859 } else static if (is (Element == uint)) { 860 enum type = napi_typedarray_type.napi_uint32_array; 861 } else static if (is (Element == float)) { 862 enum type = napi_typedarray_type.napi_float32_array; 863 } else static if (is (Element == double)) { 864 enum type = napi_typedarray_type.napi_float64_array; 865 } else static if (is (Element == long)) { 866 enum type = napi_typedarray_type.napi_bigint64_array; 867 } else static if (is (Element == ulong)) { 868 enum type = napi_typedarray_type.napi_biguint64_array; 869 } else { 870 static assert (0, `Cannot make TypedArray of ` ~ Element.stringof); 871 } 872 } 873 874 private size_t tArrLastId = 0; 875 876 import core.memory : GC; 877 private extern (C) void onTypedArrayFinalize ( 878 napi_env env 879 , void * finalizeData 880 , void * hint 881 ) { 882 GC.removeRoot (finalizeData); 883 } 884 885 /// Note: The array data must be kept alive in D. 886 napi_status typedArrayToNapi (T)( 887 napi_env env 888 , ref TypedArray!T array 889 , napi_value * toRet 890 ) { 891 napi_value arrayBuffer; 892 napi_finalize finalizeCb; 893 auto arrPtr = array.internal.ptr; 894 // Keep it alive on D side; 895 GC.addRoot (arrPtr); 896 // Also ensure that a moving collector does not relocate the object. 897 GC.setAttr (arrPtr, GC.BlkAttr.NO_MOVE); 898 899 auto status = napi_create_external_arraybuffer ( 900 env 901 , arrPtr 902 , T.sizeof * array.internal.length 903 , & onTypedArrayFinalize 904 , null 905 , & arrayBuffer 906 ); 907 assert (status == napi_status.napi_ok); 908 909 return napi_create_typedarray ( 910 env 911 , TypedArray!T.type 912 , array.internal.length 913 , arrayBuffer 914 , 0 915 , toRet 916 ); 917 } 918 919 napi_status aaToNapi (V)(napi_env env, V [string] toConvert, napi_value * toRet) { 920 assert (toRet != null); 921 auto status = napi_create_object (env, toRet); 922 assert (status == napi_status.napi_ok); 923 foreach (key, value; toConvert) { 924 inJSScope! (() { 925 status = napi_set_named_property (env, *toRet, key.toStringz, value.toNapiValue (env)); 926 assert (status == napi_status.napi_ok); 927 }) (env); 928 } 929 return napi_status.napi_ok; 930 } 931 932 napi_status stringToNapi (StrType)(napi_env env, StrType toConvert, napi_value * toRet) { 933 assert (toRet != null); 934 static if (is (StrType == string)){ 935 alias NapiCharType = char; 936 alias conversionFunction = napi_create_string_utf8; 937 } else static if (is (StrType == wstring)) { 938 alias NapiCharType = ushort; 939 alias conversionFunction = napi_create_string_utf16; 940 } else static assert ( 941 false 942 , `Cannot convert UTF32 strings (dstrings) to JS. Please use strings or wstrings instead` 943 ); 944 return conversionFunction ( 945 env 946 , cast (const NapiCharType *) toConvert.ptr 947 , toConvert.length 948 , toRet 949 ); 950 } 951 952 napi_status nullableToNapi (T) (napi_env env, Nullable!T toConvert, napi_value * toRet) { 953 assert (toRet != null); 954 if (toConvert.isNull ()) { 955 return napi_get_null (env, toRet); 956 } else { 957 return toNapi!T (env, toConvert.get (), toRet); 958 } 959 } 960 961 napi_status callbackToNapi (F)( 962 napi_env env 963 , napi_callback toConvert 964 , napi_value * toRet 965 , F * fPointer 966 ) { 967 assert (toRet != null); 968 return napi_create_function (env, null, 0, toConvert, fPointer, toRet); 969 } 970 971 napi_status delegateToNapi (Dg)(napi_env env, Dg * toCall, napi_value * toRet) { 972 assert (toRet != null); 973 static assert (isDelegate!(Dg)); 974 return callbackToNapi (env, &fromJsPtr! (Dg), toRet, toCall); 975 } 976 977 napi_status callableToNapi (F)(napi_env env, F toCall, napi_value * toRet) { 978 assert (toRet != null); 979 static assert (!isDelegate!(F), `Use delegateToNapi instead`); 980 return callbackToNapi (env, &fromJsPtr!F, toRet, toCall); 981 } 982 983 napi_status jsObjToNapi (T)(napi_env env, T toConvert, napi_value * toRet) { 984 assert (toRet != null); 985 assert (env == toConvert.env); 986 *toRet = toConvert.context; 987 return napi_status.napi_ok; 988 } 989 990 napi_status algebraicToNapi (T ...)(napi_env env, VariantN!T toConvert, napi_value * toRet) { 991 static assert (T.length > 1); 992 assert (toRet != null); 993 foreach (possibleType; T [1..$]) { 994 auto valueTried = toConvert.peek!possibleType; 995 if (valueTried != null) { 996 *toRet = toNapiValue (*valueTried, env); 997 return napi_status.napi_ok; 998 } 999 } 1000 assert (0, `Could not get value from Algebraic/VariantN`); 1001 } 1002 1003 napi_status jsVarToNapi (napi_env env, JSVar toConvert, napi_value * toRet) { 1004 assert (toRet != null); 1005 assert (env == toConvert.env, `JS environments don't match`); 1006 *toRet = toConvert.context (); 1007 return napi_status.napi_ok; 1008 } 1009 1010 napi_status tupleToNapi (T)(napi_env env, const auto ref T toConvert, napi_value * toRet) { 1011 assert (toRet != null); 1012 napi_create_object (env, toRet); 1013 foreach (i, field; toConvert.expand) { 1014 (*toRet).p (env, toConvert.fieldNames [i], field); 1015 } 1016 return napi_status.napi_ok; 1017 } 1018 1019 napi_status structToNapi (S)(napi_env env, const auto ref S toConvert, napi_value * toRet) { 1020 assert (toRet != null); 1021 napi_create_object (env, toRet); 1022 foreach (fieldName; FieldNameTuple!S) { 1023 (*toRet).p (env, fieldName, __traits (getMember, toConvert, fieldName)); 1024 } 1025 return napi_status.napi_ok; 1026 } 1027 1028 template toNapi (alias T) { 1029 import std.typecons : Tuple; 1030 static if (is (T == bool)) { 1031 alias toNapi = boolToNapi; 1032 } static if (is (T == double)) { 1033 alias toNapi = napi_create_double; 1034 } else static if (is (T == int)) { 1035 alias toNapi = napi_create_int32; 1036 } else static if (is (T == uint)) { 1037 alias toNapi = napi_create_uint32; 1038 } else static if (is (T == long)) { 1039 alias toNapi = napi_create_int64; 1040 } else static if (is (T == ulong)) { 1041 alias toNapi = napi_create_bigint_uint64; 1042 } else static if (is (T : double)) { 1043 alias toNapi = napi_create_double; 1044 } else static if (is (T == V [string], V)) { 1045 alias toNapi = aaToNapi; 1046 } else static if (isSomeString!T) { 1047 alias toNapi = stringToNapi; 1048 } else static if (is (T == Nullable!A, A)) { 1049 alias toNapi = nullableToNapi!A; 1050 } else static if (is (T == napi_value)) { 1051 alias toNapi = napiIdentity; 1052 } else static if (is (T == Dg*, Dg)) { 1053 static if (isDelegate!Dg) { 1054 alias toNapi = delegateToNapi; 1055 } else static if (isFunctionPointer!T){ 1056 alias toNapi = callableToNapi; 1057 } else { 1058 static assert (0, `Not implemented: Conversion to JS type for ` ~ T.stringof); 1059 } 1060 } else static if (isDelegate!T) { 1061 static assert ( 1062 0 1063 , `Delegates must be sent as pointers to the delegate because of memory management` 1064 ); 1065 } else static if (is (ExternC!T == napi_callback)) { 1066 static if (!is (T == ExternC!T)) { 1067 static assert (0, `Please use extern (C) for napi callbacks`); 1068 } 1069 alias toNapi = callbackToNapi; 1070 } else static if (is (T == TypedArray!A, A)) { 1071 alias toNapi = typedArrayToNapi; 1072 } else static if (isStaticArray!T) { 1073 alias toNapi = arrayToNapi; 1074 } else static if (is (T == A[], A)) { 1075 alias toNapi = arrayToNapi; 1076 } else static if (__traits(hasMember, T, `dlangNodeIsJSObj`)) { 1077 alias toNapi = jsObjToNapi; 1078 } else static if (isVariantN!T) { 1079 alias toNapi = algebraicToNapi; 1080 } else static if (__traits (isSame, TemplateOf!T, Tuple)) { 1081 alias toNapi = tupleToNapi; 1082 } else static if (__traits (isPOD, T)) { 1083 alias toNapi = structToNapi; 1084 } else { 1085 static assert (0, `Not implemented: Conversion to JS type for ` ~ T.stringof); 1086 } 1087 } 1088 1089 napi_value toNapiValue (F)( 1090 F toCast, napi_env env 1091 ) { 1092 napi_value toRet; 1093 auto status = toNapi!F (env, toCast, &toRet); 1094 if (status != napi_status.napi_ok) { 1095 env.throwInJS (`Unable to create JS value for: ` ~ toCast.to!string); 1096 } 1097 return toRet; 1098 } 1099 1100 napi_value undefined (napi_env env) { 1101 napi_value toRet; 1102 if (napi_get_undefined (env, &toRet) != napi_status.napi_ok) { 1103 env.throwInJS (`Unable to return void (undefined in JS)`); 1104 } 1105 return toRet; 1106 } 1107 1108 bool isUndefined (napi_env env, napi_value val) { 1109 napi_valuetype valueType; 1110 assert (napi_typeof (env, val, &valueType) == napi_status.napi_ok); 1111 return valueType == napi_valuetype.napi_undefined; 1112 } 1113 1114 private auto convertNapiSignature (F, alias toFinish)( 1115 napi_env env 1116 , napi_callback_info info 1117 , void ** delegateDataPtr = null 1118 ) { 1119 alias FunParams = Parameters!F; 1120 static if (FunParams.length > 0 && is (FunParams [0] == napi_env)) { 1121 immutable argCount = FunParams.length - 1; 1122 // First parameter is the env, which is from the 'env' param in withNapiExpectedSignature. 1123 // Thus it isn't stored as an napi_value. 1124 enum envParam = `env, `; 1125 enum firstValParam = 1; 1126 } else { 1127 immutable argCount = FunParams.length; 1128 enum envParam = ``; 1129 enum firstValParam = 0; 1130 } 1131 napi_value [argCount] argVals; 1132 size_t argCountMut = argCount; 1133 auto status = napi_get_cb_info ( 1134 env 1135 , info 1136 , &argCountMut 1137 , argVals.ptr 1138 , null 1139 , delegateDataPtr 1140 ); 1141 if (status != napi_status.napi_ok) { 1142 napi_throw_type_error ( 1143 env 1144 , null 1145 , (`Failed to parse arguments for function ` ~ F.mangleof ~ `, incorrect amount?`) 1146 .toStringz 1147 ); 1148 } 1149 static foreach (i, Param; FunParams [firstValParam .. $]) { 1150 // Create a temporary value with the casted data. 1151 mixin (`auto param` ~ i.to!string ~ ` = fromNapi!Param (env, argVals [i]);`); 1152 } 1153 // Now call Function with each of these casted values. 1154 import std.range; 1155 enum paramCalls = envParam ~ iota (argCount) 1156 .map!`"param" ~ a.to!string` 1157 .joiner (`,`) 1158 .to!string; 1159 static if (isDelegate!toFinish) { 1160 assert (delegateDataPtr != null); 1161 auto tmp = cast (F**) delegateDataPtr; 1162 toFinish = **tmp; 1163 } 1164 enum toMix = q{toFinish (} ~ paramCalls ~ q{)}; 1165 enum retsVoid = Returns! (F, void); 1166 static if (retsVoid) { 1167 mixin (toMix ~ `;`); 1168 return undefined (env); 1169 } else { 1170 mixin (q{return } ~ toMix ~ q{.toNapiValue (env);}); 1171 } 1172 } 1173 1174 extern (C) napi_value fromJsPtr (F)(napi_env env, napi_callback_info info) { 1175 // It's a function pointer type, so must allocate it. 1176 F toCall; 1177 return convertNapiSignature! (F, toCall) (env, info, cast (void **) & toCall); 1178 } 1179 1180 extern (C) napi_value withNapiExpectedSignature (alias Function)( 1181 napi_env env 1182 , napi_callback_info info 1183 ) { 1184 return convertNapiSignature!(typeof(Function), Function) (env, info, null); 1185 } 1186 1187 template Returns (alias Function, OtherType) { 1188 enum Returns = is (ReturnType!Function == OtherType); 1189 } 1190 1191 extern (C) alias void func (napi_env); 1192 template MainFunction (alias Function) { 1193 alias ToCall = Function; 1194 static assert ( 1195 is (ExternC! (typeof (Function)) == func) 1196 , `MainFunction must be instantiated with a void function (napi_env)` 1197 ); 1198 } 1199 1200 bool isMainFunction (alias Function) () if (isCallable!Function) { 1201 return false; 1202 } 1203 import std.meta; 1204 bool isMainFunction (alias Function) () if (!isCallable!Function) { 1205 static if (__traits (compiles, TemplateOf!(Function))) { 1206 return __traits (isSame, TemplateOf!(Function), MainFunction); 1207 } else { 1208 return false; 1209 } 1210 } 1211 1212 mixin template exportToJs (Exportables ...) { 1213 import node_api; 1214 import js_native_api; 1215 import std.string : toStringz; 1216 import std.traits; 1217 1218 extern (C) napi_value exportToJs (napi_env env, napi_value exports) { 1219 import core.runtime; 1220 Runtime.initialize (); 1221 auto addExportable (alias Exportable)() { 1222 napi_status status; 1223 napi_value fn; 1224 status = napi_create_function ( 1225 env 1226 , null 1227 , 0 1228 , & withNapiExpectedSignature!Exportable 1229 , null 1230 , &fn 1231 ); 1232 if (status != napi_status.napi_ok) { 1233 napi_throw_error (env, null, "Was not able to wrap native function"); 1234 } else { 1235 const fnName = Exportable.mangleof; 1236 status = napi_set_named_property (env, exports, fnName.toStringz, fn); 1237 if (status != napi_status.napi_ok) { 1238 napi_throw_error ( 1239 env 1240 , null 1241 , ("Unable to populate exports for " ~ fnName).toStringz 1242 ); 1243 } 1244 } 1245 return status; 1246 } 1247 static foreach (i, alias Exportable; Exportables) { 1248 static if (isCallable!Exportable) { 1249 if (addExportable!Exportable () != napi_status.napi_ok) { 1250 debug stderr.writeln (`Error registering function to JS`); 1251 return exports; 1252 } 1253 } else static if (isMainFunction!Exportable ()) { 1254 // Just call it here (on module load). 1255 Exportable.ToCall (env); 1256 } else { 1257 const fieldName = (Exportables [i]).stringof.toStringz; 1258 // It's a field. 1259 // pragma (msg, `Got field ` ~ Exportable.stringof); 1260 auto toExp = Exportable.toNapiValue (env); 1261 auto status = napi_set_named_property (env, exports, fieldName, toExp); 1262 } 1263 } 1264 return exports; 1265 } 1266 1267 // From the C macros that register the module. 1268 1269 extern (C) static __gshared napi_module _module = { 1270 1 // nm_version 1271 , 0 // nm_flags 1272 , __FILE__.ptr 1273 , &.exportToJs 1274 , "NODE_GYP_MODULE_NAME" 1275 , null 1276 }; 1277 1278 version (Windows) { version (DigitalMars) { 1279 void main () {} // Dunno why it's needed but whatever 1280 }} 1281 1282 extern (C) pragma (crt_constructor) export __gshared void _register_NAPI_MODULE_NAME () { 1283 napi_module_register (&_module); 1284 } 1285 }